package com.xiyang.security.config.security.authorization;

import com.xiyang.security.entity.Permission;
import com.xiyang.security.repository.PermissionRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.util.*;

/**
 * 权限资源映射的数据源 <br/>
 * 这里重写并实现了基于数据库的权限数据源 <br/>
 * 实现了 {@link FilterInvocationSecurityMetadataSource} 接口 <br/>
 * 框架的默认实现是 {@link org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource} <br/>
 *
 * 权限资源 SecurityMetadataSource
 * 要实现动态的权限验证，需要先有对应的访问权限资源。
 * spring security 是通过SecurityMetadataSource来加载访问时所需要的具体权限，所以需要实现SecurityMetadataSource
 * SecurityMetadataSource是一个接口，同时FilterInvocationSecurityMetadataSource
 * 且FilterInvocationSecurityMetadataSource只是一个标识接口，对应与FilterInvocation，本身没有任何内容
 * 因为spring security中web使用的很多参数类型都是FilterInvocationSecurityMetadataSource，所以实际实现的接口都是
 * FilterInvocationSecurityMetadataSource
 *
 * SecurityMetadataSource
 * 需要实现
 * 可以识别ConfigAttribute
 * 用于给定安全对象调用的。
 *
 * AopInfrastructureBean
 * 标记接口，指示作为spring aop 基础结构一部分的bean。
 * 注意，这意味着任何这样的bean都不受自动代理的约束，即使切入点匹配。
 *
 * @author xiyang.ycj
 * @since Jun 26, 2019 09:56:56 AM
 */
@Service
public class CustomInvocationSecurityMetadataSourceService implements FilterInvocationSecurityMetadataSource {

    private static final Logger log = LoggerFactory.getLogger(CustomInvocationSecurityMetadataSourceService.class);

    private final PermissionRepository permissionRepository;
    /* key 是url+method ,value 是对应url资源的角色列表 */
    private  Map<RequestMatcher, Collection<ConfigAttribute>> requestMap = new LinkedHashMap<>();

    @Autowired
    public CustomInvocationSecurityMetadataSourceService(PermissionRepository permissionRepository) {
        this.permissionRepository = permissionRepository;
    }

    /**
     *
     *  注意：
     *  @PostConstruct 用于在依赖关系注入完成之后需要执行的方法，以执行任何初始化。
     *  此方法必须在将类放入服务之前调用，且只执行一次。
     */
    @PostConstruct
    public void init(){
        log.info("[自定义权限资源数据源]：{}","初始化权限资源");
        List<Permission> permissions = permissionRepository.findAll();
        permissions.forEach(item->{
            Set<String> roleNames = item.getRoleNames();
            List<ConfigAttribute> configAttributes = new ArrayList<>();
            for (String roleName : roleNames) {
                configAttributes.add(new SecurityConfig(roleName));
            }
            requestMap.put(new AntPathRequestMatcher(item.getUrl()),configAttributes);
        });
        System.out.println(requestMap.toString());
    }


    /**
     * getAttributes方法返回本次访问需要的权限，可以有多个权限。
     * 在上面的实现中如果没有匹配的url直接返回null，
     * 也就是没有配置权限的url默认都为白名单，想要换成默认是黑名单只要修改这里即可。
     *
     * 访问配置属性（ConfigAttribute）用于给定安全对象（通过的验证）
     *
     * @param object 安全的对象
     * @return 用于传入的安全对象的属性。 如果没有适用的属性，则应返回空集合。
     * @throws IllegalArgumentException 如果传递的对象不是SecurityDatasource实现支持的类型，则抛出异常
     */
    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        log.info("[自定义权限资源数据源]：{}","获取本次访问需要的权限");
        if(requestMap.isEmpty()){
            init();
        }
        final HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
        for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap.entrySet()) {
            if (entry.getKey().matches(request)) {
                log.info("[自定义权限资源数据源]：当前路径[{}]需要的资源权限:[{}] ==> 触发鉴权决策管理器",entry.getKey(),entry.getValue().toString());
              return entry.getValue();
            }
        }
       log.info("[自定义权限资源数据源]：{}==> {}","白名单路径",request.getRequestURI());
        return null;
    }


    /**
     *
     *
     * getAllConfigAttributes方法如果返回了所有定义的权限资源，
     * Spring Security会在启动时校验每个ConfigAttribute是否配置正确，不需要校验直接返回null。
     *
     *
     * 如果可用，则返回由实现类定义的所有ConfigAttribute。
     *
     * AbstractSecurityInterceptor使用它对针对它ConfigAttribute的每个配置属性执行启动时验证。
     *
     * @return ConfigAttribute，如果没有适用的，就返回null
     */
    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        Set<ConfigAttribute> allAttributes = new HashSet<>();
        for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap.entrySet()) {
            allAttributes.addAll(entry.getValue());
        }
        log.info("[自定义权限资源数据源]：获取所有的角色==> {}",allAttributes.toString());
        return allAttributes;
    }

    /**
     * AbstractSecurityInterceptor 调用
     * supports方法返回类对象是否支持校验，web项目一般使用FilterInvocation来判断，或者直接返回true。
     *
     * @param clazz 正在查询的类
     * @return 如果实现可以处理指定的类，则为true
     */
    @Override
    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}
